Esercitazione 1
La caratteristica principale del programmare in assembler è che le operazioni a disposizione sono solo quelle messe a disposizione dal processore. Infatti, l'assemblatore fa molto poco: dopo aver sostituito le varie label con indirizzi, traduce ciascuna istruzione, nell'ordine in cui sono presenti, nel diretto corrispettivo binario (il cosiddetto linguaggio macchina). Questo binario è poi eseguito direttamente dal processore. Dato un algoritmo per risolvere un problema, i passi base di questo algoritmo devono essere istruzioni comprese dal processore, e siamo quindi limitati dall'hardware e le sue caratteristiche.
Per esempio, dato che il processore non supporta mov
da un indirizzo di memoria a un'altro indirizzo di memoria, non possiamo fare questa operazione con una sola istruzione: dobbiamo invece scomporre in mov src, %eax
mov %eax, dest
, assicurandoci nel frattempo di non aver perso alcun dato importante prima contenuto in %eax
.
Per svolgere gli esercizi, bisogna quindi imparare a scomporre strutture di programmazione già note (come if-then-else, cicli, accesso a vettore) nelle operazioni elementari messe ad disposizione dal processore, usando il limitato numero di registri a disposizione al posto di variabili, e tenendo presente quali operazioni da fare con quali dati, senza un sistema di tipizzazione ad aiutarci.
Premesse per programmi nell'ambiente del corso
Unica eccezione alla logica di cui sopra sono i sottoprogrammi di ingresso/uscita, forniti tramite utility.s
:
questi interagiscono con il terminale tramite il kernel usando il meccanismo delle interruzioni, concetti che avrete il tempo di esplorare in corsi successivi.
Qui ci limiteremo a seguirne le specifiche per leggere o stampare a video numeri, caratteri, o stringhe.
Per esempio, parte di queste specifiche è l'uso del carattere di ritorno carrello \r
come terminatore di stringa.
Per usarli, però, va istruito l'assemblatore di aggiungere questi sottoprogrammi al nostro codice, con
.include "./files/utility.s"
Un altro aspetto importante è dove comincia e finisce il nostro programma:
nell'ambiente del corso, il punto di ingresso è la label _main
e quello di uscita è la corrispendente istruzione ret
.
Per motivi di debugging, che saranno chiari più avanti, si tende a cominciare il programma con una istruzione nop
.
Inoltre, la distinzione tra zona .data
e .text
è importante.
Dato che durante l'esecuzione sono entrambi caricati in memoria, per motivi di sicurezza il kernel Linux ci impedirà di eseguire indirizzi in .data
o di scrivere in indirizzi in .text
.
Dimenticarsi di dichiararli porta ad eccezioni durante l'esecuzione.
Infine, l'assemblatore non vede di buon occhio la mancanza di una riga vuota alla fine del file. Per evitare messaggi di warning inutili, meglio aggiungerla.
Detto ciò, possiamo quindi comprendere il programma di test, che non fa che stampare "Ok." a terminale e poi termina:
.include "./files/utility.s"
.data
messaggio: .ascii "Ok.\r"
.text
_main:
nop
lea messaggio, %ebx
call outline
ret
Le istruzioni di questa sezione sono relative all'ambiente del corso.
La direttiva .include "./files/utility.s"
ricopia il codice del file utility.s
, fornito nell'ambiente del corso.
Le specifiche dei sottoprogrammi (uso dei registri, \r
come carattere di terminazione, etc.) sono conseguenza di come è scritto questo codice, che ha a che fare con scelte del corso, tra cui la retrocompatibilità con il vecchio ambiente DOS.
L'uso di _main
e ret
(peraltro, senza alcun valore di ritorno), così come il comportamento del terminale, sono anche questi relativi all'ambiente usato.
Non sono assolutamente concetti validi in generale, per altri assembler e altri ambienti.
Esercizio 1.1
Partiamo da un esercizio con le seguenti specifiche
1. Leggere messaggio da terminale.
2. Convertire le lettere minuscole in maiuscolo.
3. Stampare messaggio modificato.
I passi 1 e 3 sono da svolgersi usando i sottoprogrammi inline
e outline
.
Cominciamo riservando in memoria, nella sezione data, spazio per le due stringhe.
.data
msg_in: .fill 80, 1, 0
msg_out: .fill 80, 1, 0
Per la lettura useremo
mov $80, %cx
lea msg_in, %ebx
call inline
Per la scrittura invece useremo
lea msg_out, %ebx
call outline
Quel che manca ora è il punto 2. Dobbiamo (capire come) fare diverse cose:
- ricopiare
msg_in
inmsg_out
carattere per carattere - controllare tale carattere, per capire se è una lettera minuscola
- se sì, cambiare tale carattere nella corrispondente maiuscola
Partiamo dal primo di questi punti, e per semplicità, scriviamone il codice ignorando i restanti due punti, ossia ricopiando il carattere indipendentemente dal fatto che sia minuscolo o maiuscolo.
Come scorrere i due vettori? Abbiamo due opzioni: usare un indice per accesso indicizzato, o due puntatori da incrementare.
Anche sulla condizione di terminazione abbiamo due opzioni: fermarsi dopo aver processato il carattere di ritorno carrello \r
, o dopo aver processato 80 caratteri.
Per questo esercizio, scegliamo la prima opzione per entrambe le scelte. Se usassimo C, scriveremmo questo:
char[] msg_in, msg_out;
...
int i = 0;
char c;
do {
c = msg_in[i];
msg_out[i] = c;
i++;
} while (c != '\r')
In assembler, questo si può scrivere così:
lea msg_in, %esi
lea msg_out, %edi
mov $0, %ecx
loop:
movb (%esi, %ecx), %al
movb %al, (%edi, %ecx)
inc %ecx
cmp $0x0d, %al
jne loop
Ci sono diversi aspetti da sottolineare.
Il primo è che nell'accesso con indice, a differenza del C, abbiamo completo controllo sia di come è calcolato l'indirizzo di accesso, sia sulla dimensione della lettura in memoria. Prendiamo il caso di movb (%esi, %ecx), %al
.
Ricordiamo che il formato dell'indirizzazione con indice è offset(%base, %indice, scala)
, dove l'indirizzo è calcolato come offset + %base + (%indice * scala)
.
Dunque (%esi, %ecx)
è, implicitamente, 0(%esi, %ecx, 1)
, dove l'1 indica il fatto che ci spostiamo di un un byte alla volta.
Dato l'indirizzo, però, in abbiamo controllo di quanti byte leggere, questa volta tramite il suffisso b
o, implicitamente, tramite la dimensione del registro di destinazione %al
.
In C, questi aspetti sono legati al fatto di usare il tipo char
, che è appunto di 1 byte.
In assembler, dobbiamo starci attenti noi.
Prima di passare al resto del punto 2, vale la pena provare a comporre il programma così com'è, testarlo ed eseguirlo. Infatti, è sempre una buona idea trovare i bug quanto prima, e quanto più è semplice il codice scritto tanto più lo è trovare la fonte del bug.
Ci rimane ora da controllare che il carattere letto sia una minuscola, e nel caso cambiarla in maiuscola.
Per il primo punto, ci basta ricordare che i caratteri ASCII hanno una codifica binaria ordinata: char c
è minuscola se c >= 'a' && c <= 'z'
.
Per cambiare invece una minuscola e maiuscola, notiamo sempre dalla tabella ASCII che la distanza tra 'a'
e 'A'
, è la stessa di qualunque altra coppia di maiuscola-minuscola; ci basta infatti sottrarre 32 ad una minuscola per ottenere la corrispondente maiuscola, e aggiungere 32 per fare il contrario.
Guardando alla rappresentazione in base 2, notiamo che l'operazione è ancora più semplice: essendo , si tratta di mettere il bit in posizione 5 a 0 o 1, usando and
, or
o xor
con maschere appropriate.
Detto ciò, il codice C diventa:
char[] msg_in, msg_out;
...
int i = 0;
char c;
do {
c = msg_in[i];
if(c >= 'a' && c <= 'z')
c = c & 0xdf;
msg_out[i] = c;
i++;
} while (c != '\r')
Dove 0xdf
corrisponde a 1101 1111
, ossia l'and
resetta il bit in posizione 5.
Domanda: se vogliamo che tutte le lettere siano maiuscole, non basta resettare il bit 5 a prescindere, e non fare il controllo?
Risposta: no, perché ci sono altri caratteri ASCII con il bit 5 a 1 che non sono affatto lettere. Per esempio, il carattere spazio di codifica 0x20
.
Questo si traduce nel seguente assembler:
lea msg_in, %esi
lea msg_out, %edi
mov $0, %ecx
loop:
movb (%esi, %ecx), %al
cmp $'a', %al
jb post_check
cmp $'z', %al
ja post_check
and $0xdf, %al # 1101 1111 -> l'and resetta il bit 5
post_check:
movb %al, (%edi, %ecx)
inc %ecx
cmp $0x0d, %al
jne loop
Notiamo che le due condizione nell'if vanno rimaneggiate per essere tradotte, infatti saltiamo a dopo la conversione se le condizioni non sono verificate.
Il codice finale è quindi il seguente, scaricabile qui come file sorgente.
.include "./files/utility.s"
.data
msg_in: .fill 80, 1, 0
msg_out: .fill 80, 1, 0
.text
_main:
nop
punto_1:
mov $80, %cx
lea msg_in, %ebx
call inline
nop
punto_2:
lea msg_in, %esi
lea msg_out, %edi
mov $0, %ecx
loop:
movb (%esi, %ecx), %al
cmp $'a', %al
jb post_check
cmp $'z', %al
ja post_check
and $0xdf, %al
post_check:
movb %al, (%edi, %ecx)
inc %ecx
cmp $0x0d, %al
jne loop
punto_3:
lea msg_out, %ebx
call outline
nop
fine:
ret
Le label punto_1
, punto_2
, punto_3
e fine
sono, come è facile verificare, del tutto opzionali.
Sono però utili ai fini del debugging, che presentiamo ora.
Sono da notare le nop
aggiunte prima tra le call
alle righe 13 e 33 e le successive label:
queste sono un workaround per ovviare ad un problema di gdb
, che spiegherò più avanti.
Uso del debugger
Debugging is like being the detective in a crime movie where you are also the murderer.
Filipe Fortes
La parola debugger suggerisce da sé che sia uno strumento per rimuovere bug ma, purtroppo, questo non vuol dire che lo strumento li rimuove da solo. Infatti, quello in cui ci è utile il debugger è trovare i bug, seguendo l'esecuzione del programma passo passo e controllando il suo stato per capire dov'è che il suo comportamento differisce da quanto ci aspettiamo. Da lì, spesso indagando a ritroso e con un po' di intuito, si può trovare le istruzioni incriminate e correggerle.
Domanda: sembra complicato, non è più facile rileggere il codice?
Risposta: sì, lo è. Ma, in genere, quando basta rileggere è perché si è fatto un errore di digitazione, non di ragionamento. Saper usare il debugger significa sapersi tirare fuori velocemente da errori che richiederebbero rileggere a fondo tutto il codice.
Il debugger che usiamo è gdb
, che funziona da linea di comando.
Questo parte da un binario eseguibile, che verrà eseguito passo passo come da noi indicato.
Per semplicità d'uso, l'ambiente ha uno script debug.ps1
, da lanciare con
./debug.ps1 nome-eseguibile
Lo script fa dei controlli, tra cui assicurarsi che si sia passato l'eseguibile e non il sorgente, lancia il debugger con alcuni comandi tipici già inseriti (imposta un breakpoint a _main
e lancia il programma), e ne definisce altri per comodità d'uso (rr
e qq
, per riavviare il programma o uscire senza dare conferma).
Vediamo come usarlo, lanciando il debugger sul programma realizzato nell'esercizio precedente. Dopo un sezione di presentazione del programma, abbiamo del testo del tipo
Breakpoint 1, _main () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:9
9 nop
(gdb)
Un breakpoint è un punto del programma, in genere una linea di codice, dove si desidera che il debugger fermi l'esecuzione.
Avendo impostato il primo breakpoint a _main
, vediamo infatti che il programma si ferma alla prima istruzione relativa, che è appunto la nop
.
Importante: il debugger si ferma prima dell'esecuzione della riga indicata.
Vediamo poi che il debugger richiede input: infatti possiamo interagire con il debugger solo quando il programma è fermo. Possiamo fare tre cose in particolare:
- Osservare il contenuto di registri e indirizzi di memoria (
info registers
ex
), - Impostare nuovi breakpoints (
break
), - Continuare l'esecuzione in modo controllato (
step
enext
) o fino al prossimo breakpoint (continue
)
Vediamoli in azione. Cominciamo con il proseguire fino alla riga 13.
Breakpoint 1, _main () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:9
9 nop
(gdb) step
punto_1 () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:11
11 mov $80, %cx
(gdb) s
12 lea msg_in, %ebx
(gdb) s
13 call inline
(gdb)
Notiamo che gdb
accetta sia comandi per esteso sia abbreviati, per esempio per step
va bene anche s
.
Con questi 3 step, abbiamo eseguito le prime tre istruzioni ma non la call
a riga 13.
Possiamo controllare lo stato dei registri usando info registers
, abbreviabile con i r
.
(gdb) i r
eax 0x66 102
ecx 0x50 80
edx 0x2d 45
ebx 0x56559066 1448448102
esp 0xffffc06c 0xffffc06c
ebp 0xffffc078 0xffffc078
esi 0xf7fb2000 -134537216
edi 0xf7fb2000 -134537216
eip 0x5655676e 0x5655676e <punto_1+10>
eflags 0x282 [ SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb)
Notare: è un caso trovare i registri già inizializzati a 0, come qui mostrato.
Questo ci da info su diversi registri, molti dei quali non ci interessano. Possiamo specificare quali registri vogliamo, anche di dimensioni minori di 32 bit.
(gdb) i r cx ebx
cx 0x50 80
ebx 0x56559066 1448448102
(gdb)
La prossima istruzione, se lasciamo il programma eseguire, è una call
.
In questo caso, abbiamo due scelte: proseguire nella chiamata al sottoprogramma (andando quindi alle istruzioni di inline
, definite in utility.s
), o oltre la chiamata, andando quindi direttamente alla riga 14.
Questa è la differenza fra step
e next
: step
prosegue dentro i sottoprogrammi, mentre next
prosegue finché il sottoprogramma non ritorna.
È qui però che è rilevante la presenza della nop
aggiunta a riga 14, prima di parte_2
.
next
infatti continua fino alla prossima istruzione della sezione corrente del codice, che è in questo caso punto_1
.
Se però tale sezione termina subito dopo la call, e non esiste quindi una successiva istruzione nella stessa sezione, allora usando next
il programma continuerà fino alla terminazione.
Aggiungere la nop
ovvia al problema essendo una successiva istruzione ancora parte di punto_1
.
13 call inline
(gdb) n
questo e' un test
14 nop
(gdb)
Da notare che "questo e' un test" è proprio l'input inserito da tastiera durante l'esecuzione di inline
.
Eseguire il programma un'istruzione alla volta può risultare molto lento.
Per esempio, quando vogliamo osservare cosa succede ad una particolare iterazione di un loop.
Per questo ci aiutano break
e continue
.
Nell'esempio che segue, sono usati per raggiungere rapidamente la quarta iterazione.
(gdb) b loop
Breakpoint 2 at 0x56556785: file /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s, line 20.
(gdb) c
Continuing.
Breakpoint 2, loop () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:20
20 movb (%esi, %ecx), %al
(gdb) i r ecx
ecx 0x0 0
(gdb) c
Continuing.
Breakpoint 2, loop () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:20
20 movb (%esi, %ecx), %al
(gdb) i r ecx
ecx 0x1 1
(gdb) c
Continuing.
Breakpoint 2, loop () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:20
20 movb (%esi, %ecx), %al
(gdb) c
Continuing.
Breakpoint 2, loop () at /mnt/c/reti_logiche/assembler/lezioni/1/maiusc.s:20
20 movb (%esi, %ecx), %al
(gdb) i r ecx
ecx 0x3 3
(gdb)
L'ultima operazione base da vedere è osservare valori in memoria.
Il comando x
sta per examine memory ma, a differenza degli altri comandi, esiste solo in forma abbreviata.
Il comando ha 4 argomenti:
N
, il numero di "celle" consecutive della memoria da leggere;F
, il formato con cui interpretare il contenuto di tali "celle", per esempiod
per decimale ec
per ASCII;U
, la dimensione di ciascuna "cella":b
per 1 byte,h
per 2 byte,w
per 4 byte;addr
, l'indirizzo in memoria da cui cominciare la lettura.
Il formato del comando è x/NFU addr
.
Gli argomenti N
, F
e U
sono, di default, gli ultimi utilizzati. Questo è infatti un comando con memoria.
Quando non sono specificati, si dovrà omettere anche lo /
.
L'argomento addr
si può passare come
- costante esadecimale, per esempio
x 0x56559066
; - label preceduta da
&
, per esempiox &msg_in
; - registro preceduto da
$
, per esempiox $esi
; - espressione basata su aritmetica dei puntatori, per esempio
x (int*)&msg_in+$ecx
.
L'ultima opzione è abbastanza ostica da sfruttare, vedremo come evitarla con una tecnica alternativa.
Vediamo degli esempi tornando al debugging del nostro primo programma:
(gdb) x/20cb &msg_in
0x56559066: 113 'q' 117 'u' 101 'e' 115 's' 116 't' 111 'o' 32 ' ' 101 'e'
0x5655906e: 39 '\'' 32 ' ' 117 'u' 110 'n' 32 ' ' 116 't' 101 'e' 115 's'
0x56559076: 116 't' 13 '\r' 10 '\n' 0 '\000'
(gdb) x/20cb &msg_out
0x565590b6: 81 'Q' 85 'U' 69 'E' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x565590be: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x565590c6: 0 '\000' 0 '\000' 0 '\000' 0 '\000'
(gdb) x/20cb $esi
0x56559066: 113 'q' 117 'u' 101 'e' 115 's' 116 't' 111 'o' 32 ' ' 101 'e'
0x5655906e: 39 '\'' 32 ' ' 117 'u' 110 'n' 32 ' ' 116 't' 101 'e' 115 's'
0x56559076: 116 't' 13 '\r' 10 '\n' 0 '\000'
In questo programma usiamo un'indirizzazione con indice per leggere e scrivere lettere nei vettori.
Infatti, vediamo che il registro esi
punta sempre alla prima lettera del vettore, e abbiamo bisogno di usare anche ecx
per sapere qual'è la lettera che il programma intende processare in questa iterazione del loop.
Per usare la sintassi menzionata sopra, dovremmo ricordarci come tradurre (%esi, %ecx)
in un'espressione di aritmetica dei puntatori.
Una alternativa molto agevole è invece la scomposizione dell'istruzione movb (%esi, %ecx), %al
in due: una lea
e una mov
.
Infatti, ricordiamo che la lea
ci permette di calcolare un indirizzo, anche se con composto con indice, e salvarlo in un registro.
Possiamo per esempio scrivere
lea (%esi, %ecx), %ebx
movb (%ebx), %al
In questo modo, l'indirizzo della lettera da leggere sarà contenuto in ebx
, cosa che possiamo sfruttare nel debugger con il comando x/1cb $ebx
.
Come ultime indicazioni sul debugger, menzioniamo il comando layout regs
, che mostra ad ogni passo i registri e il codice da eseguire, e i comandi r
, per riavviare il programma e q
, per terminare il debugger.
Le versioni qq
e rr
, definite ad hoc nell'ambiente di questo corso, fanno lo stesso senza richiedere conferma.
Esercizio 1.2: istruzioni stringa
L'esercizio precedente compie un'operazione ripetuta su vettori. Legge da un vettore, una cella alla volta, ne manipola il contenuto, poi lo scrive su un altro vettore. Questo genere di operazioni è adatto per l'uso delle istruzioni stringa.
Provare a svolgere da sé l'esercizio, prima di andare oltre.
1. Leggere messaggio da terminale.
2. Convertire le lettere minuscole in maiuscolo, usando le istruzioni stringa.
3. Stampare messaggio modificato.
Le istruzioni stringa sono un esempio di set di istruzioni specializzate, cioè istruzioni che non sono pensate per impementare algoritmi generici, ma sono invece pensate per fornire supporto hardware efficiente ad uno specifico set di operazioni che alcuni algoritmi necessitano. Infatti, ci si può aspettare che tra due programmi equivalenti, uno scritto con sole istruzioni generali e l'altro scritto con istruzioni specializzate, il secondo sarà molto più performante del primo. Altri esempi comuni sono le istruzioni a supporto di crittografia, encoding e decoding di stream multimediali, e, più recentemente, neural networks.
Questi set di istruzioni sono però più "rigidi" delle istruzioni ad uso generale. Ci impongono infatti dei modi specifici di organizzare dati e codice, perché questi devono essere compatibili con il modo in cui l'algoritmo eseguito da un'istruzione è implementato in hardware.
Nell'esercizio precedente abbiamo considerato due modi di scorrere i due array. Nel primo, che è quello che abbiamo scelto, si carica l'indirizzo di inizio del vettore, e si usa un altro registro come indice, usando l'indirizzazione con indice. Nel secondo, si usa un registro come puntatore alla cella corrente, inizializzato all'indirizzo di inizio del vettore e poi incrementato (della quantità giusta) per passare all'elemento successivo. In entrambi i casi, siamo liberi di usare i registri che vogliamo, per esempio non abbiamo nessun problema se scriviamo il programma di prima come segue:
lea msg_in, %eax
lea msg_out, %ebx
mov $0, %edx
loop:
movb (%eax, %edx), %cl
...
Infatti, usare esi
ed edi
come registri puntatori, ed ecx
come registro di indice, è del tutto opzionale.
Tutto questo cambia quando si vogliono esare istruzioni specializzate come le istruzioni stringa.
Queste ci impongono di usare esi
come puntatore al vettore sorgente, edi
come puntatore al vettore destinatario, eax
come registro dove scrivere o da cui leggere il valore da trasferire, ecx
come contatore delle ripetizioni da eseguire, etc.
Una volta scelte le istruzioni da usare, dobbiamo quindi assicurarci di seguire quanto imposto dall'istruzione.
Per questo esercizio siamo interessati alla lods
, che legge un valore dal vettore e ne sposta il puntatore allo step successivo, e la stos
, che scrive un valore nel vettore.
Partiamo dal riscrivere il punto_2
in modo da rendere l'algoritmo compatibile.
...
punto_2:
lea msg_in, %esi
lea msg_out, %edi
loop:
movb (%esi), %al
inc %esi
cmp $'a', %al
jb post_check
cmp $'z', %al
ja post_check
and $0xdf, %al
post_check:
movb %al, (%edi)
inc %edi
cmp $0x0d, %al
jne loop
...
Abbiamo dunque rimosso l'uso di ecx
come indice, e usiamo esi
ed edi
come puntatori.
Il fatto di usare la inc
è legato alla dimensione dei dati, cioè 1 byte.
Dovremmo invece scrivere add $2, %esi
o add $4, %esi
per dati su 2 o 4 byte.
Altra nota è che incrementiamo i puntatori, anziché decrementarli, perché stiamo eseguendo l'operazione da sinistra verso destra.
Siamo pronti adesso a sostiture le istruzioni evidenziate con delle istruzioni stringa. Il sorgente finale è scaricabile qui.
...
punto_2:
lea msg_in, %esi
lea msg_out, %edi
cld
loop:
lodsb
cmp $'a', %al
jb post_check
cmp $'z', %al
ja post_check
and $0xdf, %al
post_check:
stosb
cmp $0x0d, %al
jne loop
...
L'istruzione cld
serve a impostare a 0 il flag di direzione, che serve a indicare alle istruzioni stringa se andare da sinistra verso destra o il contrario.
Dato che tutti i registri sono impliciti, dobbiamo sempre specificare la dimensione delle istruzioni, in questo caso b
.
Come esercizio, può essere interessante osservare con il debugger l'evoluzione dei registri, osservando come si eseguono più operazioni con una sola istruzione.
Esercizi per casa
Parte fondamentale delle esercitazioni è fare pratica. Per questo, vengono lasciati alcuni esercizi per casa.
Esercizi 1.3 e 1.4
Scrivere dei programmi che si comportano come gli esercizi 1.1 e 1.2, tranne che per il fatto di convertire da maiuscolo in minuscolo anziché il contrario.
Esercizio 1.5
Scrivere un programma che, a partire dalla sezione .data
che segue (e scaricabile qui), conta e stampa il numero di occorrenze di numero
in array
.
.include "./files/utility.s"
.data
array: .word 1, 256, 256, 512, 42, 2048, 1024, 1, 0
array_len: .long 9
numero: .word 1
Esercizio 1.6
Quello che segue (e scaricabile qui) è un tentativo di soluzione dell'esercizio precedente. Contiene tuttavia uno o più bug. Trovarli e correggerli.
.include "./files/utility.s"
.data
array: .word 1, 256, 256, 512, 42, 2048, 1024, 1, 0
array_len: .long 9
numero: .word 1
.text
_main:
nop
mov $0, %cl
mov numero, %ax
mov $0, %esi
comp:
cmp array_len, %esi
je fine
cmpw array(%esi), %ax
jne poi
inc %cl
poi:
inc %esi
jmp comp
fine:
mov %cl, %al
call outdecimal_byte
ret
Esercizio 1.7
Scrivere un programma che svolge quanto segue.
# leggere 2 numeri interi in base 10, calcolarne il prodotto, e stampare il risultato.
# lettura:
# come primo carattere leggere il segno del numero, cioè un '+' o un '-'
# segue il modulo del numero, minore di 256
# stampa:
# stampare prima il segno del numero (+ o -), poi il modulo in cifre decimali
Esercizio 1.8
Quello che segue (e scaricabile qui) è un tentativo di soluzione dell'esercizio precedente. Contiene tuttavia uno o più bug. Trovarli e correggerli.
.include "./files/utility.s"
mess1: .asciz "inserire il primo numero intero:\r"
mess2: .asciz "inserire il secondo numero intero:\r"
mess3: .asciz "il prodotto dei due numeri e':\r"
a: .word 0
b: .word 0
_main:
nop
lea mess1, %ebx
call outline
call in_intero
mov %ax, a
lea mess2, %ebx
call outline
call in_intero
mov %ax, b
mov a, %ax
mov b, %bx
imul %bx
lea mess3, %ebx
call outline
call out_intero
ret
# legge un intero composto da segno e modulo minore di 256
# ne lascia la rappresentazione in complemento alla radice base 2 in ax
in_intero:
push %ebx
mov $0, %bl
in_segno_loop:
call inchar
cmp $'+', %al
je in_segno_poi
cmp $'-', %al
jne in_segno_loop
mov $1, %bl
in_segno_poi:
call outchar
call indecimal_word
call newline
cmp $1, %bl
jne in_intero_fine
neg %ax
in_intero_fine:
pop %ebx
ret
# legge la rappresentazione di un numero intero in complemento alla radice base 2 in eax
# lo stampa come segno seguito dalle cifre decimali
out_intero:
push %ebx
mov %eax, %ebx
cmp $0, %ebx
ja out_intero_pos
jmp out_intero_neg
out_intero_pos:
mov $'+', %al
call outchar
jmp out_intero_poi
out_intero_neg:
mov $'-', %al
call outchar
neg %ebx
jmp out_intero_poi
out_intero_poi:
mov %ebx, %eax
call outdecimal_long
pop %ebx
ret